/*
 * WaveCanvas.java
 *
 * Copyright (c) 2010 Jaymie Strecker (strecker at cs.umd.edu). All rights reserved.
 *
 * This file is part of Tonic.
 *
 * Tonic is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Tonic is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Tonic.  If not, see <http://www.gnu.org/licenses/>.
 */

package tonic.gui;

import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyListener;
import tonic.audio.Player;
import tonic.audio.Excerpt;
import tonic.error.InternalException;
import java.awt.event.MouseListener;

/**
 * WaveCanvas is a panel that displays the wave form for some audio, the 
 * play position, and the excerpts the user has marked or is marking.
 * It has three modes: no file open, playing, and paused. In play mode, the user can mark
 * a new excerpt by holding down a key. In pause mode, the user can click on
 * an excerpt to show it in a ChordDisplay. In no-file-open mode, the canvas is blank. 
 *
 * @author Jaymie Strecker (strecker at cs.umd.edu)
 */
public class WaveCanvas extends JPanel implements KeyListener, MouseListener {
    private static final int DEFAULT_HEIGHT = 100;
    private static final int DEFAULT_PIXELS_PER_SECOND = 200;
    private static final Color DEFAULT_BACKGROUND_COLOR = Color.BLACK;
    private static final Color DEFAULT_WAVE_COLOR = Color.GREEN;
    private static final Color DEFAULT_PLAY_POSITION_COLOR = Color.BLUE;
    private static final Color DEFAULT_EXCERPT_FILL_COLOR = Color.RED;
    private static final Color DEFAULT_EXCERPT_BORDER_COLOR = Color.WHITE;
    private static final Color DEFAULT_EXCERPT_CLICKED_COLOR = Color.WHITE; 
    private volatile boolean isPlaying;
    private volatile boolean isMarking;
    private ChordDisplay chordDisplay;
    private int[] yMin = null;
    private int[] yMax = null;
    private int numSamples; 
    private float samplesPerSecond;
    private float absMaxSample;
    private int width;  // max x-coordinate
    private int height; // max y-coordinate
    private int playPositionCoordinate;   // x-coordinate on canvas
    private int markStartCoordinate;
    private int lastClickedExcerptStart;  // index of excerpt most recently clicked on

    /**
     * Constructs a blank WaveCanvas in no-file-open mode.
     *
     * @param chordDisplay  the ChordDisplay that is controlled when the user clicks
     *                      on an excerpt
     */
    public WaveCanvas(ChordDisplay chordDisplay) {
        super();
        this.chordDisplay = chordDisplay;
        setFocusable(true); 
        setPreferredSize(new Dimension(0, DEFAULT_HEIGHT));
        isPlaying = false;
        isMarking = false;
        addMouseListener(this);
    }

    /**
     * Sets the mode to either playing (where the user can hold down a key to
     * mark an excerpt) or paused (where the user can click on an excerpt to
     * show it in the ChordDisplay). 
     *
     * @param isPlaying     true for play mode, false for pause mode
     */
    public void setIsPlaying(boolean isPlaying) {
        if (Player.getFile() != null) {
            this.isPlaying = isPlaying;
            if (isPlaying) {
                requestFocusInWindow();
                addKeyListener(this);
            } else {
                isMarking = false;
                removeKeyListener(this);
            }
        } else {
            InternalException ie = new InternalException("Can't set isPlaying when player is null");
            new ErrorDisplay(this, ie.getMessage(), ie);
        }
    }

    /**
     * Sets the play position according to the given value and refreshes the display.
     *
     * @param playPositionPercent   the play position, expressed as a percentage of the
     *                              total play length 
     */
    public void setPlayPosition(float playPositionPercent) {
        if (Player.getFile() != null) {
            try {
                playPositionCoordinate = Math.round(playPositionPercent * width);
                int playPositionSample = coordinate2Sample(playPositionCoordinate);
                int excerptNum = excerptContainingSample(playPositionSample);
                if (excerptNum >= 0) {
                    Excerpt excerpt = Player.getExcerpt(excerptNum);
                    if (excerpt.getRankedPitches() != null) {
                        lastClickedExcerptStart = excerpt.getStartSampleNum();
                        chordDisplay.display(excerpt.getRankedPitches());
                    }
                }
                repaint();
            } catch (InternalException e) {
                new ErrorDisplay(this, e.getMessage(), e);
            }
        } else {
            InternalException ie = new InternalException("Can't set play position when player is null");
            new ErrorDisplay(this, ie.getMessage(), ie);
        }
    }

    /**
     * Refreshes the display and state based on the Player's audio. Call whenever a new
     * audio file is loaded in the Player.
     */
    public void resetPlayer() {
        if (! isPlaying) {
            this.samplesPerSecond = Player.getSamplesPerSecond();
            this.numSamples = Player.getNumSamples();
            width = (int) (DEFAULT_PIXELS_PER_SECOND * (numSamples / samplesPerSecond));
            height = DEFAULT_HEIGHT;
            setPreferredSize(new Dimension(width, height));

            yMin = new int[width];
            yMax = new int[width];
            absMaxSample = 0;
            for (int i = 0; i < numSamples; i++) {
                absMaxSample = Math.max(absMaxSample, Math.abs(Player.getSample(i)));
            }
            float stride = numSamples / (float) yMin.length;
            for (int i = 0; i < yMin.length; i++) {
                float minInStride = 0;
                float maxInStride = 0;
                int startSample = Math.round(i * stride);
                int endSample = Math.round((i + 1) * stride);
                for (int j = startSample; j < endSample; j++) {
                    minInStride = Math.min(minInStride, Player.getSample(j));
                    maxInStride = Math.max(maxInStride, Player.getSample(j));
                }
                yMin[i] = height/2 - (int)(minInStride / absMaxSample * height/2);
                yMax[i] = height/2 - (int)(maxInStride / absMaxSample * height/2);
            }

            lastClickedExcerptStart = -1;
            repaint();
        } else {
            InternalException ie = new InternalException("Can't set player while playing");
            new ErrorDisplay(this, ie.getMessage(), ie);
        }
    }

    /**
     * Calculates the width (in user coordinates) that the panel would need to be to
     * display the amount of wave form corresponding to the given number of seconds.
     *
     * @param seconds   the number of seconds
     * @return          the corresponding width, in user coordinates
     */
    public int getViewWidth(int seconds) {
        return seconds * DEFAULT_PIXELS_PER_SECOND; 
    }

    /**
     * Converts an audio sample number to an x-coordinate on the panel.
     *
     * @param sampleNum     the index of the sample
     * @return              the corresponding x-coordinate
     */
    private int sample2Coordinate(int sampleNum) {
        return Math.round(sampleNum / (float) numSamples * width);
    }

    /**
     * Converts an x-coordinate on the panel to an audio sample number.
     *
     * @param coordinate    the x-coordinate
     * @return              the corresponding sample index
     */
    private int coordinate2Sample(int coordinate) {
        return Math.round(coordinate / (float) width * numSamples);
    }

    /**
     * Paints either a blank panel (if no Player is attached) or a wave form, a
     * play-position indicator, and any excerpts that have been or are being marked. 
     */
    @Override
    public void paint(Graphics g) {
        if (yMin != null && yMax != null) {
            Graphics2D g2 = (Graphics2D) g;
            Rectangle visibleRect = getVisibleRect();
            int visibleStart = (int) visibleRect.getX();
            int visibleWidth = (int) visibleRect.getWidth();
            int visibleEnd = visibleStart + visibleWidth;

            // Background
            g2.setColor(DEFAULT_BACKGROUND_COLOR);
            g2.fillRect(visibleStart, 0, visibleWidth, height);

            // Excerpts already marked
            for (int i = 0; i < Player.getNumExcerpts(); i++) {
                Excerpt e = Player.getExcerpt(i);
                int startCoordinate = sample2Coordinate(e.getStartSampleNum());
                int endCoordinate = sample2Coordinate(e.getEndSampleNum());

                if (endCoordinate < visibleStart)  continue;
                if (visibleEnd < startCoordinate)  break;
                
                if (e.getStartSampleNum() == lastClickedExcerptStart) {
                    g2.setColor(DEFAULT_EXCERPT_CLICKED_COLOR);
                } else {
                    g2.setColor(DEFAULT_EXCERPT_FILL_COLOR);
                }
                g2.fillRect(startCoordinate, 0, endCoordinate - startCoordinate, height);
                g2.setColor(DEFAULT_EXCERPT_BORDER_COLOR);
                g2.drawRect(startCoordinate, 0, endCoordinate - startCoordinate, height);
            }

            // Excerpt being marked
            if (isMarking) {
                g2.setColor(DEFAULT_EXCERPT_FILL_COLOR);
                g2.fillRect(markStartCoordinate, 0,
                        playPositionCoordinate - markStartCoordinate + 1, height);
                g2.setColor(DEFAULT_EXCERPT_BORDER_COLOR);
                g2.drawRect(markStartCoordinate, 0,
                        playPositionCoordinate - markStartCoordinate + 1, height);
            }

            // Play position indicator
            g2.setColor(DEFAULT_PLAY_POSITION_COLOR);
            g2.drawLine(playPositionCoordinate, 0, playPositionCoordinate, height);

            // Wave form
            g2.setColor(DEFAULT_WAVE_COLOR);
            for (int x = visibleStart; x < visibleEnd; x++) {
                g2.drawLine(x, yMin[x], x, height/2);
                g2.drawLine(x, yMax[x], x, height/2);
            }
        } else {
            super.paint(g); 
        }
    }

    /**
     * Does nothing. Implemented for KeyListener interface.
     */
    public void keyTyped(KeyEvent e) {
        // nop
    }

    /**
     * If the WaveCanvas is in play mode and not already marking an excerpt, starts
     * marking one.
     *
     * @param e     ignored
     */
    public synchronized void keyPressed(KeyEvent e) {
//        System.out.println("***Key pressed");
        if (isPlaying && !isMarking) {
            try {
                Player.startMark();
            } catch (InternalException ex) {
                new ErrorDisplay(this, "There was a problem marking the excerpt.", ex); 
            }
            markStartCoordinate = sample2Coordinate(Player.getMarkStart());
            isMarking = true;
        }
    }

    /**
     * If the WaveCanvas is in play mode and is marking an excerpt, stops marking it.
     *
     * @param e     ignored
     */
    public synchronized void keyReleased(KeyEvent e) {
//        System.out.println("***Key released");
        if (isPlaying && isMarking) {
            if (Player.getSamplePosition() > Player.getMarkStart()) {
                Player.stopMark();
                isMarking = false;
            }
            // If the user is hitting multiple keys willy-nilly, or if their computer
            // is extremely slow, then keyReleased might execute immediately after
            // keyPressed, making sample position == mark start. If the user uses just
            // one finger, this doesn't seem to be an issue. 
        }
    }

    /**
     * Searches for an excerpt containing the given sample number. If one is found,
     * returns the index of the excerpt. Otherwise, returns -1.
     *
     * @param sampleNum     the sample number (index) to search for
     * @throws InternalException    if player is null
     */
    private int excerptContainingSample(int sampleNum) throws InternalException {
        if (Player.getFile() == null) {
            throw new InternalException("Can't find excerpt containing sample when player is null");
        }
        // TODO - move to Player, use binary search
        for (int i = 0; i < Player.getNumExcerpts(); i++) {
            Excerpt excerpt = Player.getExcerpt(i);
            if (excerpt.getStartSampleNum() <= sampleNum &&
                    sampleNum <= excerpt.getEndSampleNum()) {
                return i;
            }
        }
        return -1;
    }

    /**
     * If the WaveCanvas is not blank (has a Player attached) and is in pause mode,
     * selects an excerpt and tells the ChordDisplay to display it.
     *
     * @param e     ignored
     */
    public void mouseClicked(MouseEvent e) {
        if (!isPlaying && Player.getFile() != null) {
            try {
                int clickX = e.getX();
                int clickSample = coordinate2Sample(clickX);
                int excerptNum = excerptContainingSample(clickSample);
                if (excerptNum >= 0) {
                    Excerpt excerpt = Player.getExcerpt(excerptNum);
                    lastClickedExcerptStart = excerpt.getStartSampleNum();
                    repaint();
                    chordDisplay.display(excerpt.getRankedPitches());
                }
            } catch (InternalException ex) {
                new ErrorDisplay(this, ex.getMessage(), ex); 
            }
        }
    }

    /**
     * Does nothing. Implemented for MouseListener interface.
     */
    public void mousePressed(MouseEvent e) {
        // nop
    }

    /**
     * Does nothing. Implemented for MouseListener interface.
     */
    public void mouseReleased(MouseEvent e) {
        // nop
    }

    /**
     * Does nothing. Implemented for MouseListener interface.
     */
    public void mouseEntered(MouseEvent e) {
        // nop
    }

    /**
     * Does nothing. Implemented for MouseListener interface.
     */
    public void mouseExited(MouseEvent e) {
        // nop
    }

}
